/*
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.
 */
package erwiki.part.variables.internal.services;

import java.security.Principal;
import java.util.Date;
import java.util.List;
import java.util.ResourceBundle;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.log4j.Logger;
import org.elwiki_data.WikiPage;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ServiceScope;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;

import erwiki.api.Release;
import erwiki.api.WikiPartReference;
import erwiki.api.attachment.AttachmentManager;
import erwiki.api.component.WikiComponent;
import erwiki.api.core.Engine;
import erwiki.api.core.WikiContext;
import erwiki.api.core.WikiSession;
import erwiki.api.exceptions.NoSuchVariableException;
import erwiki.api.exceptions.WikiException;
import erwiki.api.filters.FilterManager;
import erwiki.api.filters.PageFilter;
import erwiki.api.modules.InternalModule;
import erwiki.api.pages.PageManager;
import erwiki.api.preferences.Preferences;
import erwiki.api.providers.WikiProvider;
import erwiki.api.rwc.RWC;
import erwiki.api.variables.VariableManager;
import erwiki.configuration.IWikiConfiguration;

/**
 * Manages variables. Variables are case-insensitive. A list of all available variables is on a Wiki
 * page called "WikiVariables".
 *
 * @since 1.9.20.
 */
//@formatter:off
@Component(
	name = "erwiki.VariableManager",
	service = { VariableManager.class, WikiComponent.class, EventHandler.class },
	scope = ServiceScope.SINGLETON)
//@formatter:on
public class DefaultVariableManager implements VariableManager, WikiComponent, EventHandler {

	private static final Logger log = Logger.getLogger(DefaultVariableManager.class);

	/**
	 * Contains a list of those properties that shall never be shown. Put names here in lower case.
	 */
	static final String[] THE_BIG_NO_NO_LIST = { "jspwiki.auth.masterpassword" };

	/**
	 * Creates a VariableManager object using the property list given.
	 *
	 * @param props The properties.
	 */
	public DefaultVariableManager() {
		super();
	}

	// -- OSGi service handling ----------------------(start)--

	/** Stores configuration. */
	@Reference
	private IWikiConfiguration wikiConfiguration;

	@WikiPartReference
	private Engine engine;

	@WikiPartReference
	private PageManager pageManager;

	@WikiPartReference
	private AttachmentManager attachmentManager;

	@WikiPartReference
	private FilterManager filterManager;

	/** {@inheritDoc} */
	@Override
	public void initialize() throws WikiException {
		// nothing to do.
	}

	// -- OSGi service handling ------------------------(end)--

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String parseAndGetValue(final WikiContext context, final String link)
			throws IllegalArgumentException, NoSuchVariableException {
		if (!link.startsWith("{$")) {
			throw new IllegalArgumentException("Link does not start with {$");
		}
		if (!link.endsWith("}")) {
			throw new IllegalArgumentException("Link does not end with }");
		}
		final String varName = link.substring(2, link.length() - 1);

		return getValue(varName.trim());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	// FIXME: somewhat slow.
	public String expandVariables(final String source) {
		final StringBuilder result = new StringBuilder();
		for (int i = 0; i < source.length(); i++) {
			if (source.charAt(i) == '{') {
				if (i < source.length() - 2 && source.charAt(i + 1) == '$') {
					final int end = source.indexOf('}', i);

					if (end != -1) {
						final String varname = source.substring(i + 2, end);
						String value;

						try {
							value = getValue(varname);
						} catch (final NoSuchVariableException | IllegalArgumentException e) {
							value = e.getMessage();
						}

						result.append(value);
						i = end;
					}
				} else {
					result.append('{');
				}
			} else {
				result.append(source.charAt(i));
			}
		}

		return result.toString();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String getValue(final String varName, final String defValue) {
		try {
			return getValue(varName);
		} catch (final NoSuchVariableException e) {
			return defValue;
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public String getVariable(final String name) {
		return getValue(name, null);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	/* TODO: убрать "WikiContext context" - доработать context.getVariable()
	   - код не должен быть привязан к этому контексту. */
	public String getValue(String varName) throws IllegalArgumentException, NoSuchVariableException {
		if (varName == null) {
			throw new IllegalArgumentException("Null variable name.");
		}
		if (varName.length() == 0) {
			throw new IllegalArgumentException("Zero length variable name.");
		}

		try {
			// Faster than doing equalsIgnoreCase()
			final String name = varName.toLowerCase();

			for (final String value : THE_BIG_NO_NO_LIST) {
				if (name.equals(value)) {
					return ""; // FIXME: Should this be something different?
				}
			}

			String value = getInternalVariable(name);
			if (value != null) {
				// Запрошенная переменная - внутренняя переменная wiki.
				return value;
			}

			//
			// It is not a system var. Time to handle the other cases.
			//
			// Check if such a context variable exists, returning its string representation.
			//
			Object var = RWC.INSTANCE.getVariable(varName);
			if (var != null) {
				return var.toString();
			}

			//
			// Well, I guess it wasn't a final straw. We also allow variables from the session and the request
			// (in this order).
			//
			final HttpServletRequest req = RWC.INSTANCE.getRequest();
			if (req != null && req.getSession() != null) {
				final HttpSession session = req.getSession();

				try {
					String s = (String) session.getAttribute(varName);

					if (s != null) {
						return s;
					}

					s = req.getParameter(varName);
					if (s != null) {
						return s;
					}
				} catch (final ClassCastException e) {
					log.debug("Not a String: " + varName);
				}
			}

			//
			// And the final straw: see if the current page has named metadata.
			//
			WikiPage pg = RWC.INSTANCE.getPage();
			if (pg != null) {
				final Object metadata = pg.getAttribute(varName);
				if (metadata != null) {
					return metadata.toString();
				}
			}

			//
			// And the final straw part 2: see if the "real" current page has named metadata. This allows
			// a parent page to control a inserted page through defining variables
			//
			WikiPage rpg = RWC.INSTANCE.getRealPage();
			if (rpg != null) {
				final Object metadata = rpg.getAttribute(varName);
				if (metadata != null) {
					return metadata.toString();
				}
			}

			//
			// Next-to-final straw: attempt to fetch using property name. We don't allow fetching any other
			// properties than those starting with "jspwiki.". I know my own code, but I can't vouch for bugs
			// in other people's code... :-)
			//
			if (varName.startsWith("jspwiki.")) {
				final String s = wikiConfiguration.getWikiPreferences().getString(varName);
				if (s != null && s.length() > 0) {
					return s;
				}
			}

			//
			// Final defaults for some known quantities.
			//
			if (varName.equals(VAR_ERROR) || varName.equals(VAR_MSG)) {
				return "";
			}

		} catch (Exception e) {
			log.info("Interesting exception: cannot fetch variable value", e);
		}

		throw new NoSuchVariableException("No variable " + varName + " defined.");
	}

	/**
	 * В случае, если заданное имя - не внутренняя переменная, то возвращает <code>null</code>.
	 *
	 * @param name Имя переменной.
	 * @return Значение внутренней переменной Wiki, или <code>null</code>.
	 */
	private String getInternalVariable(String name) {
		return switch (name) {
		case "pageid" -> RWC.INSTANCE.getPage().getId();

		case "pagename" -> RWC.INSTANCE.getPage().getName();

		case "applicationname" -> engine.getApplicationName();

		case "elwikiversion" -> Release.getVersionString();

		case "encoding" -> engine.getContentEncoding().displayName();

		case "totalpages" -> Integer.toString(pageManager.getTotalPageCount());

		case "pageprovider" -> {
			WikiProvider p = pageManager.getCurrentProvider();
			yield (p != null) ? p.getClass().getName() : "-";
		}

		case "pageproviderdescription" -> pageManager.getProviderDescription();

		case "attachmentprovider" -> {
			WikiProvider p = attachmentManager.getCurrentProvider();
			yield (p != null) ? p.getClass().getName() : "-";
		}

		case "attachmentproviderdescription" -> {
			WikiProvider p = attachmentManager.getCurrentProvider();
			yield (p != null) ? p.getProviderInfo() : "-";
		}

		case "interwikilinks" -> {
			StringBuilder res = new StringBuilder();
			for (String link : wikiConfiguration.getAllInterWikiLinks()) {
				if (res.length() > 0) {
					res.append(", ");
				}
				res.append(link);
				res.append(" --> ");
				res.append(wikiConfiguration.getInterWikiURL(link));
			}
			yield res.toString();
		}

		/*:FVK: AllInlinedImagePatterns - надо взять из JSPWikiMarkupParser
		case "inlinedimages" -> String.join(", ", engine.getAllInlinedImagePatterns());
		*/

		case "baseurl" -> wikiConfiguration.getBaseURL();

		case "uptime" -> {
			Date now = new Date();
			long secondsRunning = (now.getTime() - engine.getStartTime().getTime()) / 1_000L;
			long seconds = secondsRunning % 60;
			long minutes = (secondsRunning /= 60) % 60;
			long hours = (secondsRunning /= 60) % 24;
			long days = secondsRunning /= 24;
			yield days + "d, " + hours + "h " + minutes + "m " + seconds + "s";
		}

		case "loginstatus" -> {
			WikiSession session = RWC.INSTANCE.getWikiSession();
			ResourceBundle rcBundle = Preferences.getBundle();
			yield rcBundle.getString("varmgr." + session.getLoginStatus().getId());
		}

		case "username" -> {
			Principal wup = RWC.INSTANCE.getWikiSession().getUserPrincipal();
			ResourceBundle rcBundle = Preferences.getBundle();
			yield wup != null ? wup.getName() : rcBundle.getString("varmgr.not.logged.in");
		}

		case "requestcontext" -> RWC.INSTANCE.getCommand().getRequestContext();

		case "pagefilters" -> {
			List<PageFilter> filters = filterManager.getFilterList();
			StringBuilder sb = new StringBuilder();
			for (PageFilter pf : filters) {
				String f = pf.getClass().getName();
				if (pf instanceof InternalModule) {
					continue;
				}

				if (sb.length() > 0) {
					sb.append(", ");
				}
				sb.append(f);
			}
			yield sb.toString();
		}

		default -> null;
		};
	}

	@Override
	public void handleEvent(Event event) {
		/*String topic = event.getTopic();
		switch (topic) {
		}*/
	}

}
